package com.dcz.fileportal;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.dcz.fileportal.exceptions.DirectoryProvidedException;
import com.dcz.fileportal.exceptions.EmptyFileException;
import com.dcz.fileportal.exceptions.UploadFailedException;
import com.dcz.fileportal.network.CountingFileRequestBody;
import com.dcz.fileportal.network.ProgressListener;
import com.dcz.fileportal.network.bean.Response;
import com.dcz.fileportal.network.callbacks.FileUploadResultCallback;
import com.dcz.fileportal.options.UploadOptions;
import com.dcz.fileportal.utils.ContentType;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;

/**
 * 文件传送门。
 * todo 注意：目前后台存在文件size限制，最大值为20MB。
 */
public class FilePortal {

    private static volatile FilePortal mInstance;
    private OkHttpClient mClient;
    private final Gson mGson;
    private SharedPreferences mSP;
    private final ExecutorService mExecutor;
    private final Handler mHandler;
    private final int RET_SUCCESS = 200;
    static final String NET_KEY_FILE = "file";

    private FilePortal() {
        initOkHttpClient();
        mGson = new Gson();
        mExecutor = Executors.newCachedThreadPool();
        mHandler = new Handler(Looper.getMainLooper());
    }

    /**
     * Initialize the OkHttp Client which is used globally.
     */
    private void initOkHttpClient() {
        mClient = new OkHttpClient.Builder()
                .build();
    }

    /**
     * Get the singleton instance.
     *
     * @return Singleton instance of FilePortal.
     */
    public static FilePortal getInstance() {
        if (mInstance == null) {
            synchronized (FilePortal.class) {
                if (mInstance == null) {
                    mInstance = new FilePortal();
                }
            }
        }
        return mInstance;
    }

    /**
     * Exposed api for upload file.
     *
     * @param uploadOptions Options of the upload task.
     */
    public void upload(@NonNull UploadOptions uploadOptions) {
        final File file = uploadOptions.getFile();
        final String api = uploadOptions.getApi();
        final HashMap<String, String> headerMap = uploadOptions.getHeaderMap();
        final HashMap<String, String> map = uploadOptions.getMap();
        final FileUploadResultCallback callback = uploadOptions.getFileUploadResultCallback();
        final ProgressListener progressListener = uploadOptions.getProgressListener();
        upload(api, file, headerMap, map, callback, progressListener);
    }

    /**
     * Get token and start uploading the file.
     *
     * @param file             The file to upload.
     * @param callback         File upload result callback.
     * @param progressListener The listener of uploading progress.
     */
    private void upload(@NonNull final String api,
                        final @NonNull File file, @NonNull final HashMap<String, String> headerMap,@NonNull final HashMap<String, String> map, @Nullable final FileUploadResultCallback callback,
                        @Nullable ProgressListener progressListener) {
        if (file.isDirectory()) {
            onFailureCallback(callback, new DirectoryProvidedException());
            return;
        }
        if (file.length() == 0) {
            onFailureCallback(callback, new EmptyFileException());
            return;
        }
        Runnable runnable = () -> {
            doUpload(api, file, headerMap, map, callback, progressListener);

        };
        mExecutor.execute(runnable);
    }


    /**
     * Upload the file to the backend server.
     *
     * @param file             The file to be uploaded.
     * @param callback         The callback of file uploading result.
     * @param progressListener The listener of uploading progress.
     */
    private void doUpload(@NonNull final String api, @NonNull final File file, final HashMap<String, String> headerMap, final HashMap<String, String> map, @Nullable final FileUploadResultCallback callback
            , @Nullable ProgressListener progressListener) {
        final MediaType mediaType = MediaType.parse(ContentType.getContentTypeFromExtension(file.getName(), ContentType.IMAGE_PREFIX));//todo 如何选择一个合适的默认值。
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart(NET_KEY_FILE, file.getName(), RequestBody.create(mediaType, file));
        for (Map.Entry<String, String> entry : map.entrySet()) {
            builder.addFormDataPart(entry.getKey(), entry.getValue());
        }

        if (progressListener != null) {
            builder.addPart(new CountingFileRequestBody(file, percent -> onProgressCallback(progressListener, percent)));
        }
        RequestBody requestBody = builder.build();
        Request.Builder requestBuilder = new Request.Builder();
        for (Map.Entry<String, String> entry : headerMap.entrySet()) {
            requestBuilder.addHeader(entry.getKey(), entry.getValue());
        }
        Request request = requestBuilder
                .url(api)
                .post(requestBody)
                .build();
        try (okhttp3.Response response = mClient.newCall(request).execute()) {
            if (response.isSuccessful() && response.body() != null) {
                String body = response.body().string();
                Log.e("->FilePortal", "doUpload@155 --> " + body);
                Response<Response.UploadBean> uploadInfo = mGson.fromJson(body, new TypeToken<Response<Response.UploadBean>>() {
                }.getType());
                //Succeed to upload the file.
                if (uploadInfo.getRet() == RET_SUCCESS) {
                    onSuccessCallback(callback, uploadInfo.getData().getUrl(), false, uploadInfo.getData().getRequestId());
                    return;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            onFailureCallback(callback, e);
            return;
        }
        onFailureCallback(callback, new UploadFailedException());
    }

    /**
     * Post success info to main thread when the file is uploaded successfully.
     *
     * @param callback The callback of file uploading result.
     * @param url      The download url of the uploaded file.
     * @param isCached if the file cached by the backend server.
     */
    private void onSuccessCallback(@Nullable FileUploadResultCallback callback, String url, boolean isCached, String request_id) {
        if (callback != null) {
            mHandler.post(() -> callback.onSuccess(url, isCached, request_id));
        }
    }

    /**
     * Post failure info to main thread when the file failed to upload.
     *
     * @param callback The callback of file uploading result.
     * @param e        The exception for which file uploaded failed.
     */
    private void onFailureCallback(@Nullable FileUploadResultCallback callback, Exception e) {
        if (callback != null) {
            mHandler.post(() -> callback.onFailure(e));
        }
    }

    /**
     * Post the progress to main thread when uploading progress changed.
     *
     * @param progressListener The listener of file uploading progress.
     * @param progress         Current progress of the uploading task.
     */
    private void onProgressCallback(@Nullable ProgressListener progressListener, int progress) {
        if (progressListener != null) {
            mHandler.post(() -> progressListener.transferred(progress));
        }
    }

    /**
     * Shut down the executor.
     */
    public void shutDownAllTasks() {
        if (!mExecutor.isShutdown())
            mExecutor.shutdown();
    }

    /**
     * Shut down the executor.
     */
    public void shutDownAllTasksNow() {
        if (!mExecutor.isShutdown())
            mExecutor.shutdownNow();
    }

}
